package cn.zwx.distributed.svc.service.impl;

import cn.zwx.base.exception.BizWrapException;
import cn.zwx.distributed.svc.domain.bo.BitsAllocatorBO;
import cn.zwx.distributed.svc.domain.enums.DistributedCodeEnum;
import cn.zwx.distributed.svc.service.UidGeneratorService;
import cn.zwx.distributed.svc.service.WorkerIdService;
import cn.zwx.distributed.svc.utils.DateUtils;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @author zhangwenxue
 */
@Data
@Service
public class DefaultUidGeneratorServiceImpl implements UidGeneratorService, InitializingBean {
    public static final Logger logger = LoggerFactory.getLogger(DefaultUidGeneratorServiceImpl.class);

    /** Bits allocate */
    private int timeBits = 29;
    private int workerBits = 21;
    private int seqBits = 13;

    /** Customer epoch, unit as second. For example 2016-05-20 (ms: 1463673600000)*/
    private String epochStr = "2016-05-20";
    private long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(1463673600000L);

    /** Stable fields after spring bean initializing */
    private BitsAllocatorBO   bitsAllocator;
    private long workerId;

    /** Volatile fields caused by nextId() */
    protected long sequence = 0L;
    protected long lastSecond = -1L;

    /** Spring property */
    @Resource
    private WorkerIdService workerIdService;


    @Override
    public void afterPropertiesSet() throws Exception {
        // initialize bits allocator
        bitsAllocator = new BitsAllocatorBO(timeBits, workerBits, seqBits);

        // initialize worker id
        workerId = workerIdService.assignWorkerId();
        if (workerId > bitsAllocator.getMaxWorkerId()) {
            throw new RuntimeException("Worker id " + workerId + " exceeds the max " + bitsAllocator.getMaxWorkerId());
        }

        logger.info("Initialized bits(1, {}, {}, {}) for workerID:{}", timeBits, workerBits, seqBits, workerId);
    }

    @Override
    public long getDistributedId() throws BizWrapException {
        return this.nextId();
    }

    @Override
    public String parsDistributedId(long uid) {
        long totalBits = BitsAllocatorBO.TOTAL_BITS;
        long signBits = bitsAllocator.getSignBits();
        long timestampBits = bitsAllocator.getTimestampBits();
        long workerIdBits = bitsAllocator.getWorkerIdBits();
        long sequenceBits = bitsAllocator.getSequenceBits();

        // parse UID
        long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
        long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
        long deltaSeconds = uid >>> (workerIdBits + sequenceBits);

        Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
        String thatTimeStr = DateUtils.formatByDateTimePattern(thatTime);

        // format as string
        return String.format("{\"UID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}",
                uid, thatTimeStr, workerId, sequence);
    }

    /**
     * Get UID
     *
     * @return UID
     * @throws BizWrapException in the case: Clock moved backwards; Exceeds the max timestamp
     */
    protected synchronized long nextId() throws BizWrapException {
        long currentSecond = getCurrentSecond();
        // Clock moved backwards, refuse to generate uid
        if (currentSecond < lastSecond) {
            long refusedSeconds = lastSecond - currentSecond;
            String refusedSecondsDesc = String.format("Clock moved backwards. Refusing for %d seconds",  refusedSeconds);
            throw new BizWrapException(DistributedCodeEnum.CLOCK_MOVED_BACKWARDS_ERROR.getCode(), refusedSecondsDesc);
        }
        // At the same second, increase sequence
        if (currentSecond == lastSecond) {
            sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
            // Exceed the max sequence, we wait the next second to generate uid
            if (sequence == 0) {
                currentSecond = getNextSecond(lastSecond);
            }
            // At the different second, sequence restart from zero
        } else {
            sequence = 0L;
        }
        lastSecond = currentSecond;
        // Allocate bits for UID
        return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
    }

    /**
     * Get next millisecond
     */
    private long getNextSecond(long lastTimestamp) throws BizWrapException {
        long timestamp = getCurrentSecond();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentSecond();
        }

        return timestamp;
    }

    /**
     * Get current second
     */
    private long getCurrentSecond() throws BizWrapException {
        long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
        if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
            String exhaustedDesc = "Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond;
            throw new BizWrapException(DistributedCodeEnum.TIMESTAMP_BITS_EXHAUSTED_ERROR.getCode(),exhaustedDesc);
        }

        return currentSecond;
    }


    public void setTimeBits(int timeBits) {
        if (timeBits > 0) {
            this.timeBits = timeBits;
        }
    }

    public void setWorkerBits(int workerBits) {
        if (workerBits > 0) {
            this.workerBits = workerBits;
        }
    }

    public void setSeqBits(int seqBits) {
        if (seqBits > 0) {
            this.seqBits = seqBits;
        }
    }

    public void setEpochStr(String epochStr) {
        if (StringUtils.isNotBlank(epochStr)) {
            this.epochStr = epochStr;
            this.epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseByDayPattern(epochStr).getTime());
        }
    }
}
